When writing code during the development of software applications, developers commonly spend a significant amount of time “debugging” the code to find runtime errors in the code. In doing so, developers may take several approaches to reproduce and locate a source code bug, such as observing behavior of a program based on different inputs, inserting debugging code (e.g., to print variable values, to track branches of execution, etc.), temporarily removing code portions, etc. Tracking down runtime errors to pinpoint code bugs can occupy a significant portion of application development time.
Many types of debugging software applications (“debuggers”) have been developed in order to assist developers with the code debugging process. These tools offer developers the ability to trace, visualize, and alter the execution of computer code. For example, debuggers may visualize the execution of code instructions, may present variable values at various times during code execution, may enable developers to alter code execution paths, and/or may enable developers to set “breakpoints” and/or “watchpoints.”
For most debuggers, a “breakpoint” triggers when a specified point in computer code (i.e., a specific code instruction) is reached, and a “watchpoint” triggers when the value of a specified data element has changed or is accessed. When a breakpoint or watchpoint triggers, the debugger suspends execution of the subject code, and presents program state at that point. To implement breakpoints/watchpoints, a debugger identifies a memory address associated with the item of interest (e.g., a memory address associated with a code instruction for breakpoints, or a memory address associated with a data element for watchpoints), and then watches for an access to that address. While breakpoints and watchpoints are distinct, for simplicity they are both referred to generally herein as breakpoints. Thus, as used herein, a “breakpoint” refers to any debugger functionality that causes a specific memory address to be watched for, and that suspends code execution based on detecting an access to a watched-for memory address.
When watching for memory accesses, debuggers may utilize breakpoint functionality provided by a processor architecture (e.g., special processor registers devoted to breakpoint functionality), and/or may instrument the code that is being executed such that each operation that accesses memory (e.g., a read or a write to memory storing data, or a read/fetch for execution from memory storing program code) is instrumented with additional code instructions that check whether or not the access would cause a breakpoint to be triggered.
As indicated above, debuggers enable breakpoints to be set on specific code instructions (e.g., often corresponding to function boundaries, flow control statements, variable assignments and instantiations, etc.), and/or on specified data elements themselves, such as language primitives (e.g., integers, booleans, characters, strings, references, etc.), composite data structures (e.g., arrays, records, unions, etc.), abstract data structures (e.g., containers, lists, trees, graphs, etc.), or other custom data structures such as instances of classes. When setting breakpoints on specified data elements, debuggers set the breakpoint on the base memory address of the subject data element. Thus, for example, a breakpoint on a linked list results in a breakpoint on the memory address of the head of the linked list, a breakpoint on an array results in a breakpoint on the memory address of the first element in the array, a breakpoint on an instance of a class results in a breakpoint on the memory address referencing the instance, etc.